package org.carabiner.infinitest;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JOptionPane;

public class RemoteProcessManager
{
  private Process process;
  private Class<? extends TestRunner> runnerClass;
  int maxMemoryInMegs = 64;
  private static final String JAVA_EXECUTABLE = "java";
  private String classpath = InfinitestUtils.getCleanClasspath(System.getProperty("java.class.path"));

  public RemoteProcessManager(Class<? extends TestRunner> testRunnerClass)
  {
    runnerClass = testRunnerClass;
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
    {
      public void run()
      {
        synchronized (RemoteProcessManager.this)
        {
          if (process != null)
            process.destroy();
        }
      }
    }));
  }

  public boolean startProcess(int port, String[] arguments) throws IOException
  {
    ProcessBuilder builder = new ProcessBuilder();
    String memorySetting = "-mx" + maxMemoryInMegs + "m";

    String[] processArgs = buildArguments(port, arguments, memorySetting);
    builder.command(processArgs);

    process = builder.start();
    return process != null;
  }

  private String[] buildArguments(int portNum, String[] additionalArguments, String memorySetting)
  {
    int predefinedArgLength = 7;
    String[] arguments = new String[predefinedArgLength + additionalArguments.length];
    arguments[0] = JAVA_EXECUTABLE;
    arguments[1] = "-cp";
    arguments[2] = classpath;
    arguments[3] = memorySetting;
    arguments[4] = SimpleRunnerProcess.class.getName();
    arguments[5] = runnerClass.getName();
    arguments[6] = String.valueOf(portNum);
    System.arraycopy(additionalArguments, 0, arguments, predefinedArgLength, additionalArguments.length);
    return arguments;
  }

  // FIXME Refactor
  // Should be extracted out into a package level class?
  private static class SimpleRunnerProcess
  {
    private ArrayList<TestEvent> eventList;
    private TestRunner runner;

    public SimpleRunnerProcess(String runnerClass)
    {
      runner = createRunner(runnerClass);
    }

    private TestRunner createRunner(String string)
    {
      TestRunner runner = null;
      try
      {
        Class runnerClass = Class.forName(string);
        runner = (TestRunner) runnerClass.newInstance();
      }
      catch (ClassNotFoundException e)
      {
        throw new RuntimeException(e);
      }
      catch (InstantiationException e)
      {
        throw new RuntimeException(e);
      }
      catch (IllegalAccessException e)
      {
        throw new RuntimeException(e);
      }
      TestStatusListener listenerProxy = (TestStatusListener) Proxy.newProxyInstance(runner.getClass().getClassLoader(), new Class[] { TestStatusListener.class }, new InvocationHandler()
      {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
        {
          eventList.add((TestEvent) args[0]);
          return null;
        }
      });
      runner.addTestStatusListener(listenerProxy);
      return runner;
    }

    private List<TestEvent> runTest(String testName)
    {
      eventList = new ArrayList<TestEvent>();
      runner.runTest(testName);
      return eventList;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException
    {
      try
      {
        SimpleRunnerProcess process = new SimpleRunnerProcess(args[0]);
        int portNum = Integer.parseInt(args[1]);
        Socket clientSocket = new Socket("127.0.0.1", portNum);
        ObjectOutputStream ooStream = new ObjectOutputStream(clientSocket.getOutputStream());
        List<TestEvent> results = new ArrayList<TestEvent>();
        for (int i = 2; i < args.length; i++)
        {
          try
          {
            results.addAll(process.runTest(args[i]));
          }
          catch (Throwable e)
          {
            results.add(createErrorEvent(process, args[i], e));
          }
        }
        ooStream.writeObject(results);
        clientSocket.close();
      }
      catch (Throwable e)
      {
        JOptionPane.showMessageDialog(null, "Fatal error in simple test runner process: " + e.getMessage());
      }
    }
  }

  public static TestEvent createErrorEvent(Object source, String testClass, Throwable error)
  {
    return new TestEvent(source, TestEvent.EventType.TEST_ERROR, error.getMessage(), testClass, "", error);
  }

  public boolean destroyProcess()
  {
    process.destroy();
    try
    {
      process.waitFor();
    }
    catch (InterruptedException e)
    {
      return false;
    }
    return true;
  }
}
